#================================================
package MSN3;
#================================================

use strict;
use warnings;

# For Debugging
use Data::Dumper;

# IO
use IO::Select;

# MSN3
use MSN3::Notification;
use MSN3::Util;

use constant VER => 'MSNP21 CVR0';
use constant CVER10 => '0x0409 winnt 6.1.0 i386 MSNMSGR 15.4.3508.1109 MSNMSGR';

my $VERSION  = 'MSN 3.0 (09/01/2011)';
my $REVISION = '$Rev: 3 $';

sub checksum { my $o = tell(DATA); seek DATA,0,0; local $/; my $t = unpack("%32C*",<DATA>) % 65535;seek DATA,$o,0; return $t;};

print $VERSION . " " . $REVISION . " - Checksum: " . (checksum() + MSN3::Util::checksum()) . "-NS" . MSN3::Notification::checksum() . "-CR" . MSN3::Circle::checksum() . "\n\n";

sub new
{
    my $class = shift;

    my $self =
    {
        # MSN Details
        Host            => 'messenger.hotmail.com',
        Port            => 1863,
        Handle          => '',
        Password        => '',

        Name            => '',
        Status          => 'NLN',

        MessageStyle    =>
        {
            Font            => 'Verdana',
            Effect          => '',
            Color           => '0',
            CharacterSet    => 0,
            PitchFamily     => 0
        },

        MachineGUID     => MSN3::CreateGUID(),

        # Events
        Handlers        => {},

        # Debugging info
        Debug           => 0,
        Error           => 1,
        ServerError     => 1,
        AutoReconnect   => 1,
        LastError       => '',

        ShowTX          => 0,
        ShowRX          => 0,

        # Connections
        Select          => new IO::Select(),
        Notification    => undef,
        Connections     => {},
        Connected       => 0,

        # Client Capabilities
        Capabilities    =>
        {
            Client          => 'MSNC11',

            # Capabilities
            WinMobile                     => 0,
            ConnectedViaTexas             => 0,
            SupportsGifInk                => 0,
            SupportsIsfInk                => 0,
            WebCamDetected                => 0,
            SupportsChunking              => 'IM',
            MobileEnabled                 => 0,
            WebWatchEnabled               => 0,
            SupportsActivities            => 0,
            OnlineViaWebIM                => 0,
            MobileDevice                  => 0,
            OnlineViaFederatedInterface   => 0,
            HasSpace                      => 0,
            IsMceUser                     => 0,
            SupportsDirectIM              => 0,
            SupportsWinks                 => 0,
            SupportsSharedSearch          => 0,
            IsBot                         => 0,
            SupportsVoiceIM               => 0,
            SupportsSChannel              => 0,
            SupportsSipInvite             => 'IM',
            SupportsVideoCall             => 0,
            SupportsSDrive                => 0,
            SupportsPageModeMessaging     => 0,
            HasOneCare                    => 0,
            P2PSupportsTurn               => 0,
            P2PBootstrapViaUUN            => 0,
            UsingAlias                    => 0,

            # Extended Capabilities
            IsSmsOnly                     => 0,
            SupportsVoiceOverMsnp         => 0,
            SupportsUucpSipStack          => 0,
            Supports1On1ViaGroup          => 0,
            SupportsApplicationMessages   => 0,
            RTCVideoEnabled               => 0,
            SupportsPeerToPeerV2          => 0,
            IsAuthenticatedWebIMUser      => 0,
            SupportsOfflineIM             => 0,
            SharingVideoSupport           => 0,
            SupportsNudges                => 'IM',
            CircleVoiceIMEnabled          => 0,
            SupportsSharing               => 0,
            SupportsPeerToPeerMixerRelay  => 'PE',
            ConvWindowFileTransfer        => 'IM',
            VideoCallSupports16x9         => 0,
            SupportsPeerToPeerEnveloping  => 'PE',
            YahooIMDisabled               => 0,
            SIPTunnelVersion2             => 0,
            SupportsMultiPartyIM          => 0,

            # Unknown Capabilities
            UNKNOWN_0x4000                => 0,
            UNKNOWN_0x10000               => 0,
            UNKNOWN_0x100000              => 0,
            UNKNOWN_0x200000              => 0,
            UNKNOWN_0x8000000             => 0,
            UNKNOWN_0x10000000            => 0,
            UNKNOWN_0x40000000            => 0,
        },
        ClientID        => '',

        @_
    };
    bless( $self, $class );

    $self->{ClientID} = MSN3::Util::setCapabilities($self->{Capabilities});

    #print Dumper(MSN3::Util::getCapabilities('2955186464:2609258384'));
    #print Dumper(MSN3::Util::getCapabilities('100663296:1142538240')); exit;

    return $self;
}

sub DESTROY
{
    my $self = shift;

    # placeholder for possible destructor code
}

sub AUTOLOAD
{
    my $self = shift;

    $self->error( "method $MSN3::AUTOLOAD not defined" ) if( $self->{AutoloadError} );
}

#================================================
# Error / Debugging
#================================================

sub error
{
    my $self = shift;
    my $message = shift || '';

    $self->{LastError} = $message;

    if( defined $self->{Handlers}->{Error} )
    {
        $self->triggerEvent( $self, 'Error', $message );
    }
    elsif( $self->{Error} )
    {
        print( "ERROR : $message\nCaller trace:\n" );

        for( my $i=0; $i<20; $i++ )
        {
            my ($package, $filename, $line, $subroutine, @more ) = caller($i);
            last if( !defined $package );
            $filename =~ s/.*MSN3/MSN3/gi;
            print( "  $i: $subroutine ($filename, line $line)\n" );
        }
    }

    return 0;
}

sub serverError
{
    my $self = shift;
    my $message = shift || '';

    if( defined $self->{Handlers}->{ServerError} )
    {
        $self->triggerEvent( $self, 'ServerError', $message );
    }
    elsif( $self->{ServerError} )
    {
        print( "SERVER ERROR : $message\n" );
    }

    return 0;
}

sub debug
{
    my $self = shift;
    my $message = shift || '';

    if( defined $self->{Handlers}->{Debug} )
    {
        $self->triggerEvent( $self, 'Debug', $message );
    }
    elsif( $self->{Debug} )
    {
        print( $message . "\n" );
    }

    return 1;
}

#================================================
# Connect
#================================================

sub connect
{
    my $self = shift;

    $self->debug( "Connecting to $self->{Host}:$self->{Port} as $self->{Handle}/$self->{Password}" );

    $self->{Notification} = new MSN3::Notification( $self, $self->{Host}, $self->{Port}, $self->{Handle}, $self->{Password} );

    if( $self->getNotification()->connect() )
    {
        $self->{Connected} = time;
    }
}

sub disconnect
{
    my $self = shift;

    foreach my $convo (values %{$self->getConvoList()})
    {
        $convo->leave();
    }

    $self->getNotification()->disconnect();
    $self->{Connected} = 0;

    return 1;
}

#================================================
# General functions
#================================================

sub getNotification
{
    my $self = shift;

    return $self->{Notification};
}

sub isConnected
{
    my $self = shift;

    return !$self->{Connected} ? 0 : 1;
}

sub uptime
{
    my $self = shift;

    return ($self->{Connected}) ? (time - $self->{Connected}) : 0;
}

sub setMessageStyle
{
    my $self = shift;

    $self->{MessageStyle} = { (%{$self->{MessageStyle}}), @_ };

    return $self->{MessageStyle};
}

sub getMessageStyle
{
    my $self = shift;

    return $self->{MessageStyle};
}

#================================================
# Event handlers
#================================================

sub setHandler
{
    my $self = shift;
    my ($event, $handler) = @_;

    $self->{Handlers}->{$event} = $handler;
}

sub setHandlers
{
    my $self = shift;
    my $handlers = { @_ };

    foreach my $event (keys %$handlers)
    {
        $self->{Handlers}->{$event} = $handlers->{$event};
    }
}

sub triggerEvent
{
    my $self = shift;
    my $receiver = shift;
    my $event = shift;

    # get and run the handler if it is defined
    my $function = $self->{Handlers}->{$event};
    return &$function( $receiver, @_ ) if( defined $function );

    # get and run the default handler if it is defined
    $function = $self->{Handlers}->{Default};
    return &$function( $receiver, $event, @_ ) if( defined $function );

    return undef;
}

#================================================
# Contact functions
#================================================

sub allowContact
{
    my $self = shift;

    $self->getNotification()->allowContact( @_ );
}

sub disallowContact
{
    my $self = shift;

    $self->getNotification()->disallowContact( @_ );
}

sub blockContact
{
    my $self = shift;

    $self->getNotification()->blockContact( @_ );
}

sub unblockContact
{
    my $self = shift;

    $self->getNotification()->unblockContact( @_ );
}

#================================================
# Misc functions
#================================================

sub setName
{
    my $self = shift;

    return $self->getNotification()->setName( @_ );
}

sub setPersonalMessage
{
  	my $self = shift;

  	return $self->getNotification()->setPersonalMessage( @_ );
}

sub findMember
{
    my $self = shift;

    return $self->getNotification()->findMember( @_ );
}

sub getConvoList
{
    my $self = shift;

    return $self->getNotification()->getConvoList( @_ );
}

sub getConvo
{
    my $self = shift;

    return $self->getNotification()->getConvo( @_ );
}

sub getConvoByHandle
{
    my $self = shift;

    return $self->getNotification()->getConvoByHandle( @_ );
}

sub sendBroadcast
{
    my $self = shift;

    return $self->getNotification()->sendBroadcast( @_ );
}

#================================================
# Internal methods
#================================================

sub CreateGUID
{
    my $self = shift;

    return lc(sprintf("{%08X-%04X-%04X-%04X-%04X%08X}",
    rand(2294967295),rand(32767),rand(32767),rand(32767),rand(32767),rand(2294967295)));
}

sub _strip_header
{
    my $self = shift;
    my $msg = shift;

    my $header = {};
    if($msg =~ /Content-Length: (.+)\r\n/gi)
    {
        my $length = $1;

        my $head = substr($msg, 0, length($msg)-$length);
        $msg = substr($msg, (length($msg)-$length));

        my @temp = split (/\r\n/, $head);
        foreach my $item (@temp)
        {
            if($item ne '')
            {
                my ($key,$value) = split(/:\s+/,$item);
                $key =~ s/[\r\n]//gi;
                $header->{$key} = $value || "";
            }
        }
    }
    elsif($msg =~ /^(.*?)\r\n\r\n(.*?)$/s)
    {
        my $head = $1;
        $msg = $2;

        my @temp = split (/\r\n/, $head);
        foreach my $item (@temp)
        {
            my ($key,$value) = split(/:\s+/,$item);
            $key =~ s/[\r\n]//gi;
            $header->{$key} = $value || "";
        }
    }

    if( defined $header->{To} )
    {
        ($header->{To}, $header->{'To-ID'}) = split(/\;epid=/, $header->{To}, 2);
        ($header->{'To-Network'}, $header->{'To-Handle'}) = split(/\:/, $header->{To}, 2);
    }
    if( defined $header->{From} )
    {
        ($header->{From}, $header->{'From-ID'}) = split(/\;epid=/, $header->{From}, 2);
        ($header->{'From-Network'}, $header->{'From-Handle'}) = split(/\:/, $header->{From}, 2);
    }

    return ($header, $msg);
}

#================================================
# The loop
#================================================

sub do_one_loop
{
    my $self = shift;

    # Return immediately if we are not connected
    return if( !$self->{Connected} );

    $self->getNotification()->ping();

    my @ready = $self->{Select}->can_read(.1);
    foreach my $fh ( @ready )
    {
        # Get the relevant connection
        my $fn = $fh->fileno;
        my $connection = $self->{Connections}->{$fn};

        if( !$connection->{Socket}->connected() )
        {
            $self->{Select}->remove( $fn );
            delete( $self->{Connections}->{fn} );
            warn "Killing dead socket";
            next;
        }

        sysread( $fh, $connection->{buf}, 2048, length( $connection->{buf} || '' ) );

        while( $connection->{buf} =~ s/^(.*?\n)// )
        {
            $connection->{line}= $1;
            my $incomingdata = $connection->{line};
            $incomingdata =~ s/[\r\n]//g;

            $self->call_event( $self, 'RX', $incomingdata );
            print( "RX: $incomingdata\n" ) if( $self->{ShowRX} );

            my $result = $connection->dispatch( $incomingdata );
            last if( $result && $result eq "wait" );
        }
    }
}

return 1;
__DATA__


=begin echobot.pl
#!/usr/bin/perl -w

use strict;
use warnings;

use lib "./lib";

# MSN 3.0
use MSN3;

my $msn = MSN3->new(
    Handle            => 'Email',
    Password          => 'Password',
    Debug             => 1,
    ShowTX            => 1,
    ShowRX            => 1
);

$msn->setHandlers(
    Connected         => \&Connected,
    Message           => \&Message
);

$msn->connect;
while(1) {
  $msn->do_one_loop;
}

sub Connected
{
    my $self = shift;
    print "Connected!\n";
}

sub Message
{
    my $self = shift;
    my ($user, $message, %style) = @_;
    $self->sendMessage($message, %style); # echo
}

=end
=cut